package club.jdiy.core.base;

import club.jdiy.core.base.domain.*;
import club.jdiy.utils.StringUtils;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.AbstractJPAQuery;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.CrudMethodMetadata;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QPageRequest;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.util.ProxyUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import java.util.*;

import static org.springframework.data.jpa.repository.query.QueryUtils.*;


/**
 * JDiy Dao 层接口实现层通用方法的默认实现
 *
 * @param <T>  Entity 类
 * @param <ID> 实体类的主键类型
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20210609
 */
public abstract class RepositoryBasic<T extends JpaEntity<ID>, ID> {

    //===================================== JDiy动态查询通用 ============================================
    public Pager<T> findPager(int pageSize, int page, T t, String order, String sort) {
        return __findPager(pageSize, page, t, order, sort);
    }

    public Pager<T> findPager(int pageSize, int page, Filterable f, String order, String sort) {
        return __findPager(pageSize, page, f, order, sort);
    }

    public List<T> findList(int size, T t, String order, String sort) {
        return __findList(size, t, order, sort);
    }

    public List<T> findList(int size, Filterable f, String order, String sort) {
        return __findList(size, f, order, sort);
    }

    public long count(T t) {
        return __count(t);
    }

    public long count(Filterable f) {
        return __count(f);
    }


    //===================================== 基础方法 ============================================
    @Deprecated
    public T getOne(ID id) {
        Assert.notNull(id, ID_MUST_NOT_BE_NULL);
        return em.getReference(getDomainClass(), id);
    }
    public T getById(ID id) {
        Assert.notNull(id, ID_MUST_NOT_BE_NULL);
        return em.getReference(getDomainClass(), id);
    }


    @NotNull
    public Optional<T> findById(@Nullable ID id) {
        if (id == null || StringUtils.isEmpty(id) || "0".equals(id) || id.equals(0)) return Optional.empty();
        Class<T> domainType = getDomainClass();
        if (metadata == null) {
            return Optional.ofNullable(em.find(domainType, id));
        }

        LockModeType type = metadata.getLockModeType();
        Map<String, Object> hints = new HashMap();
        this.getQueryHints().withFetchGraphs(this.em).forEach(hints::put);
        return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
    }

    public List<T> findAllById(Iterable<ID> ids) {
        if (ids == null || !ids.iterator().hasNext()) {
            return Collections.emptyList();
        }
        List<T> results = new ArrayList<>();
        for (ID id : ids) {
            findById(id).ifPresent(results::add);
        }
        return results;
    }

    public boolean existsById(ID id) {
        if (entityInformation.getIdAttribute() == null) {
            return findById(id).isPresent();
        }
        Assert.notNull(id, ID_MUST_NOT_BE_NULL);
        String placeholder = provider.getCountQueryPlaceholder();
        String entityName = entityInformation.getEntityName();
        Iterable<String> idAttributeNames = entityInformation.getIdAttributeNames();
        String existsQuery = QueryUtils.getExistsQueryString(entityName, placeholder, idAttributeNames);

        TypedQuery<Long> query = em.createQuery(existsQuery, Long.class);
        if (!entityInformation.hasCompositeId()) {
            query.setParameter(idAttributeNames.iterator().next(), id);
            return query.getSingleResult() == 1L;
        }

        for (String idAttributeName : idAttributeNames) {
            Object idAttributeValue = entityInformation.getCompositeIdAttributeValue(id, idAttributeName);
            boolean complexIdParameterValueDiscovered = idAttributeValue != null
                    && !query.getParameter(idAttributeName).getParameterType().isAssignableFrom(idAttributeValue.getClass());
            if (complexIdParameterValueDiscovered) {
                // fall-back to findById(id) which does the proper mapping for the parameter.
                return findById(id).isPresent();
            }
            query.setParameter(idAttributeName, idAttributeValue);
        }
        return query.getSingleResult() == 1L;
    }

    public void deleteById(ID id) {
        findById(id).ifPresent(em::remove);
    }

    public void insert(T entity) {
        em.persist(entity);
    }

    public <S extends T> S save(S entity) {
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }


    @SuppressWarnings("unchecked")
    public void delete(T entity) {
        if (entity == null || entityInformation.isNew(entity)) {
            return;
        }
        Class<?> type = ProxyUtils.getUserClass(entity);
        T existing = (T) em.find(type, entityInformation.getId(entity));
        if (existing == null) {
            return;
        }
        em.remove(em.contains(entity) ? entity : em.merge(entity));
    }

    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        List<S> result = new ArrayList<>();
        for (S entity : entities) {
            result.add(save(entity));
        }
        return result;
    }

    public void insertAll(Iterable<T> entities) {
        Iterator<T> iterator = entities.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            em.persist(iterator.next());
            index++;
            if (index % BATCH_SIZE == 0) {
                em.flush();
                em.clear();
            }
        }
        if (index % BATCH_SIZE != 0) {
            em.flush();
            em.clear();
        }
    }

    public void deleteAll(Iterable<? extends T> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        if (!entities.iterator().hasNext()) return;
        applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
                .executeUpdate();
    }

    //===================================== QueryDSL 相关方法 ============================================

    public long count(Predicate predicate) {
        lazyInit();
        return createQuery(predicate).select(path).fetchCount();
    }

    public Pager<T> findAll(Predicate predicate, Pageable pageable) {
        Assert.notNull(pageable, "Pageable must not be null!");
        lazyInit();
        JPQLQuery<T> query = querydsl.applyPagination(pageable, createQuery(predicate).select(path));
        QueryResults<T> results = query.fetchResults();
        return new Pager<>(pageable.getPageNumber() + 1, pageable.getPageSize(), (int) results.getTotal(), results.getResults());
    }

    public List<T> findAll(Predicate predicate) {
        Assert.notNull(predicate, "Predicate must not be null!");
        lazyInit();
        return createQuery(predicate).select(path).fetch();
    }

    public List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders) {
        return findAll(0, predicate, orders);
    }

    public List<T> findAll(Predicate predicate, Sort sort) {
        return this.findAll(0, predicate, sort);
    }

    public List<T> findAll(OrderSpecifier<?>... orders) {
        return this.findAll(0, orders);
    }

    private List<T> findAll(int limit, Predicate predicate, OrderSpecifier<?>... orders) {
        Assert.notNull(predicate, "Predicate must not be null!");
        Assert.notNull(orders, "Order specifiers must not be null!");
        lazyInit();
        return executeSorted(limit, createQuery(predicate).select(path), orders);
    }

    private List<T> findAll(int limit, Predicate predicate, Sort sort) {

        Assert.notNull(predicate, "Predicate must not be null!");
        Assert.notNull(sort, "Sort must not be null!");
        lazyInit();
        return executeSorted(limit, createQuery(predicate).select(path), sort);
    }

    private List<T> findAll(int limit, OrderSpecifier<?>... orders) {
        Assert.notNull(orders, "Order specifiers must not be null!");
        lazyInit();
        return executeSorted(limit, createQuery(new Predicate[0]).select(path), orders);
    }

    private List<T> executeSorted(int limit, JPQLQuery<T> query, OrderSpecifier<?>... orders) {
        return executeSorted(limit, query, new QSort(orders));
    }

    private List<T> executeSorted(int limit, JPQLQuery<T> query, Sort sort) {
        return limit > 0
                ? querydsl.applySorting(sort, query).limit(limit).fetch()
                : querydsl.applySorting(sort, query).fetch();
    }

    private JPQLQuery<?> createQuery(Predicate... predicate) {
        AbstractJPAQuery<?, ?> query = querydsl.createQuery(path);
        if (predicate != null) {
            query = query.where(predicate);
        }
        return query;
    }

    private void lazyInit() {
        if (querydsl != null) return;
        EntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        this.path = resolver.createPath(getDomainClass());
        this.querydsl = new Querydsl(em, new PathBuilder<T>(path.getType(), path.getMetadata()));
    }

    private EntityPath<T> path;
    private Querydsl querydsl;
    //=================================================================================

    //=================================================================================

    private org.springframework.data.jpa.repository.support.QueryHints getQueryHints() {
        return metadata == null ? org.springframework.data.jpa.repository.support.QueryHints.NoHints.INSTANCE : DefaultQueryHints.of(entityInformation, metadata);
    }

    protected Class<T> getDomainClass() {
        return entityInformation.getJavaType();
    }

    //===================================== 构造与私有 ============================================

    public void init(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
        Assert.notNull(entityManager, "EntityManager must not be null!");

        this.entityInformation = entityInformation;
        this.em = entityManager;
        this.provider = PersistenceProvider.fromEntityManager(entityManager);
    }


    private <F> Pager<T> __findPager(int pageSize, int page, F f, String order, String sort) {
        if (f instanceof DslFilterable) {
            DslFilter filter = getDslFilter(f);
            Pageable pageable;
            if (StringUtils.hasText(order))
                pageable = PageRequest.of(page - 1, pageSize, sort.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, order);
            else if (filter.getDefaultOrderSpecifiers() != null)
                pageable = QPageRequest.of(page - 1, pageSize, filter.getDefaultOrderSpecifiers());
            else pageable = PageRequest.of(page - 1, pageSize);
            return findAll(Optional.ofNullable(filter.getBuilder()).orElse(new BooleanBuilder()), pageable);
        } else if (f instanceof JpaFilterable) {
            JpaFilter filter = getJpaFilter((JpaFilterable) f);
            return filter.getPager(em, pageSize, page, order, sort);
        }
        return new JpaFilter(getDomainClass(), new WhereBuilder(), null).getPager(em, pageSize, page, order, sort);
    }

    private <F> List<T> __findList(int size, F f, String order, String sort) {
        if (f instanceof DslFilterable) {
            DslFilter filter = getDslFilter(f);
            BooleanBuilder bb = Optional.ofNullable(filter.getBuilder()).orElse(new BooleanBuilder());
            if (StringUtils.hasText(order))
                return findAll(size, bb, Sort.by(sort.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, order));
            else if (filter.getDefaultOrderSpecifiers() != null)
                return findAll(size, bb, filter.getDefaultOrderSpecifiers());
            else return findAll(size, bb);
        } else if (f instanceof JpaFilterable) {
            JpaFilter filter = getJpaFilter((JpaFilterable) f);
            return filter.getList(em, size, order, sort);
        }

        return new JpaFilter(getDomainClass(), new WhereBuilder(), null).getList(em, size, order, sort);
    }

    public <F> long __count(F f) {
        try {
            if (f instanceof DslFilterable) {
                DslFilter filter = getDslFilter(f);
                if (filter.getBuilder() != null) return count(filter.getBuilder());
            } else if (f instanceof JpaFilterable) {
                return getJpaFilter(f).getCount(em);
            }
            return new JpaFilter(getDomainClass(), new WhereBuilder(), null).getCount(em);
        } catch (NoResultException ignore) {
            return 0L;
        }
    }

    <F> DslFilter getDslFilter(F filterable) {
        return ((DslFilterable) filterable).createFilter(new BooleanBuilder());
    }

    <F> JpaFilter getJpaFilter(F filterable) {
        return ((JpaFilterable) filterable).createFilter(new WhereBuilder());
    }

    private static final int BATCH_SIZE = 300;
    private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
    private JpaEntityInformation<T, ?> entityInformation;
    private EntityManager em;
    private PersistenceProvider provider;
    @Nullable
    CrudMethodMetadata metadata;
}
